using System;
using System.Net;
using System.IO;
using System.Text;
using HtmlAgilityPack;
using System.Threading.Tasks;
using System.Linq;
using System.Collections.Generic;
using System.IO.Compression;
using System.Globalization;
using Locator.LcboServices.PageLoaders.Services;
using Microsoft.Practices.EnterpriseLibrary.Logging;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;

namespace Locator.LcboServices.PageLoaders.PageRetrievers
{
    public abstract class PageRetriever : IPageRetriever
    {
        private readonly static Dictionary<string, Encoding> encodingDict = BuildEncodingDictionary();
        private readonly TraceManager traceManager;
        private readonly LogWriter logWriter;

        protected PageRetriever()
        {
            this.logWriter = EnterpriseLibraryContainer.Current.GetInstance<LogWriter>();
            this.traceManager = new TraceManager(this.logWriter);
        }

        /// <summary>
        /// Gets the encodings dictionary, keyed on the charset values returned by web requests.
        /// </summary>
        /// <value>The encodings.</value>
        protected static Dictionary<string, Encoding> Encodings
        {
            get { return encodingDict; }
        }

        /// <summary>
        /// Builds the encoding dictionary.
        /// </summary>
        /// <returns></returns>
        private static Dictionary<string, Encoding> BuildEncodingDictionary()
        {
            Dictionary<string, Encoding> result = new Dictionary<string, Encoding>();
            foreach (EncodingInfo encodingInfo in Encoding.GetEncodings())
            {
                if (!result.ContainsKey(encodingInfo.Name.ToUpperInvariant()))
                {
                    result.Add(encodingInfo.Name.ToUpperInvariant(), encodingInfo.GetEncoding());
                }
            }

            return result;
        }

        /// <summary>
        /// Builds the URL.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns>The URL with the parameter in the right spot</returns>
        protected abstract Uri BuildUrl(string parameter);

        protected abstract string PostParameters { get; }

        protected abstract string RequestMethod { get; }

        protected abstract HttpWebRequest BuildRequestAsync(string paramter);

        protected abstract HttpWebRequest BuildRequest(string paramter);

        /// <summary>
        /// Retrieves the page HTML asynchronously via a POST request and returns it in a Task with an HtmlDocument.
        /// </summary>
        /// <param name="parameter">The parameter to pass to the web page.</param>
        /// <returns>A Task with an HtmlDocument</returns>
        public virtual Task<HtmlDocument> RetrievePageHtmlAsyncWithPost(string parameter)
        {
            TaskCompletionSource<HtmlDocument> pageRetrievalTaskCompletionSource = new TaskCompletionSource<HtmlDocument>();
            Uri productUri = this.BuildUrl(parameter);
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(productUri);
            request.ContentType = "application/x-www-form-urlencoded";
            if (this is LiveStoreListPageRetriever)
            {
                request.CookieContainer = new CookieContainer();
                request.CookieContainer.Add(((LiveStoreListPageRetriever)this).Cookies);
            }

            if (this is TestStoreListPageRetriever)
            {
                request.CookieContainer = new CookieContainer();
                request.CookieContainer.Add(((TestStoreListPageRetriever)this).Cookies);
            }

            //request.CookieContainer.Add()
            request.Method = this.RequestMethod;
            request.Accept = "text/html";
            request.Headers.Add("Accept-Encoding", "gzip,deflate");

            try
            {
                //Trace.TraceInformation("Beginning async request for uri {0}", request.RequestUri);
                //Stopwatch pageRetrieverStopWatch = new Stopwatch();
                //pageRetrieverStopWatch.Start();

                IAsyncResult responseResult = null;
                request.BeginGetRequestStream(getRequestStreamAsyncResult =>
                {
                    try
                    {
                        var requestStream = request.EndGetRequestStream(getRequestStreamAsyncResult);
                        string postData = this.PostParameters;
                        UTF8Encoding requestEncoding = new UTF8Encoding();
                        byte[] postDataBytes = requestEncoding.GetBytes(postData);
                        request.ContentLength = postDataBytes.Length;
                        requestStream.Write(postDataBytes, 0, postDataBytes.Length);
                        requestStream.Close();

                        responseResult = request.BeginGetResponse(getResponseAsyncResult =>
                        {
                            HttpWebResponse myWebResponse = null;
                            HtmlDocument doc = new HtmlDocument();
                            //Trace.TraceInformation("Beginning callback for uri {0}", request.RequestUri);
                            try
                            {
                                using (myWebResponse = (HttpWebResponse)request.EndGetResponse(getResponseAsyncResult))
                                {
                                    using (Stream myWebSource = GetStreamForResponse(myWebResponse))
                                    {
                                        string codePage = GetCodePage(myWebResponse.ContentType);
                                        Encoding encoding = Encodings[codePage];
                                        //Trace.TraceInformation("Beginning doc.Load for uri {0}", request.RequestUri);
                                        doc.Load(myWebSource, encoding);
                                        //pageRetrieverStopWatch.Stop();
                                        //Trace.TraceInformation("Finished doc.Load for uri {0} after {1} milliseonds", request.RequestUri, pageRetrieverStopWatch.ElapsedMilliseconds);
                                    }

                                    pageRetrievalTaskCompletionSource.SetResult(doc);
                                }
                            }
                            catch (Exception exc)
                            {
                                //Trace.TraceError("Exception in RetrievePageHtmlAsync: {0}", exc.ToString());
                                pageRetrievalTaskCompletionSource.SetException(exc);
                            }
                        }, null);
                    }
                    catch (Exception exc)
                    {
                        pageRetrievalTaskCompletionSource.SetException(exc);
                    }
                }, null);
            }
            catch (Exception exc)
            {
                //Trace.TraceError("Exception in RetrievePageHtmlAsync: {0}", exc.ToString());
                pageRetrievalTaskCompletionSource.SetException(exc);
            }

            //Trace.TraceInformation("Ending async request phase for uri {0}", request.RequestUri);
            return pageRetrievalTaskCompletionSource.Task;
        }

        /// <summary>
        /// Retrieves the page HTML asynchronously via a GET request and returns it in a Task with an HtmlDocument.
        /// </summary>
        /// <param name="parameter">The parameter to pass to the web page.</param>
        /// <returns>A Task with an HtmlDocument</returns>
        public virtual Task<HtmlDocument> RetrievePageHtmlAsyncWithGet(string parameter)
        {
            TaskCompletionSource<HtmlDocument> pageRetrievalTaskCompletionSource = new TaskCompletionSource<HtmlDocument>();
            Uri productUri = this.BuildUrl(parameter);
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(productUri);
            request.Method = this.RequestMethod;
            request.Accept = "text/html";
            request.Headers.Add("Accept-Encoding", "gzip,deflate");

            try
            {
                //Trace.TraceInformation("Beginning async request for uri {0}", request.RequestUri);
                //Stopwatch pageRetrieverStopWatch = new Stopwatch();
                //pageRetrieverStopWatch.Start();

                request.BeginGetResponse(getResponseAsyncResult =>
                {
                    HttpWebResponse myWebResponse = null;
                    HtmlDocument doc = new HtmlDocument();
                    //Trace.TraceInformation("Beginning callback for uri {0}", request.RequestUri);
                    try
                    {
                        using (myWebResponse = (HttpWebResponse)request.EndGetResponse(getResponseAsyncResult))
                        {
                            using (Stream myWebSource = GetStreamForResponse(myWebResponse))
                            {
                                string codePage = GetCodePage(myWebResponse.ContentType);
                                Encoding encoding = Encodings[codePage];
                                //Trace.TraceInformation("Beginning doc.Load for uri {0}", request.RequestUri);
                                doc.Load(myWebSource, encoding);
                                //pageRetrieverStopWatch.Stop();
                                //Trace.TraceInformation("Finished doc.Load for uri {0} after {1} milliseonds", request.RequestUri, pageRetrieverStopWatch.ElapsedMilliseconds);
                            }

                            pageRetrievalTaskCompletionSource.SetResult(doc);
                        }
                    }
                    catch (Exception exc)
                    {
                        //Trace.TraceError("Exception in RetrievePageHtmlAsync: {0}", exc.ToString());
                        pageRetrievalTaskCompletionSource.SetException(exc);
                    }
                }, null);
            }
            catch (Exception exc)
            {
                //Trace.TraceError("Exception in RetrievePageHtmlAsync: {0}", exc.ToString());
                pageRetrievalTaskCompletionSource.SetException(exc);
            }

            //Trace.TraceInformation("Ending async request phase for uri {0}", request.RequestUri);
            return pageRetrievalTaskCompletionSource.Task;
        }


        /// <summary>
        /// Parses the code page.
        /// </summary>
        /// <param name="contentType">Type of the content.</param>
        /// <returns></returns>
        protected string GetCodePage(string contentType)
        {
            string result = "utf-8";
            string[] contentTypeStrings = contentType.Split(new char[] { ';' });
            foreach (string contentTypeString in contentTypeStrings)
            {
                if (contentTypeString.Trim().StartsWith("charset", StringComparison.OrdinalIgnoreCase))
                {
                    string[] charsetStrings = contentTypeString.Split(new char[] { '=' });
                    result = charsetStrings[1];
                }
            }

            return result.ToUpperInvariant();
        }

        /// <summary>
        /// Retrieves the page HTMLS async.
        /// </summary>
        /// <param name="parameters">The parameters.</param>
        /// <param name="completionCallback">The callback.</param>
        public virtual void RetrievePageHtmlsAsync(int[] parameters, Action<Task<HtmlDocument>[]> completionCallback)
        {
            Task<HtmlDocument>[] myTasks = new Task<HtmlDocument>[parameters.Count()];
            int taskIndex = 0;
            foreach (int parameter in parameters)
            {
                if (this.RequestMethod == WebRequestMethods.Http.Post)
                {
                    myTasks[taskIndex] = this.RetrievePageHtmlAsyncWithPost(parameter.ToString(CultureInfo.InvariantCulture));
                }
                else if (this.RequestMethod == WebRequestMethods.Http.Get)
                {
                    myTasks[taskIndex] = this.RetrievePageHtmlAsyncWithGet(parameter.ToString(CultureInfo.InvariantCulture));
                }

                taskIndex++;
            }

            // The Action to call when all of myTasks are done.  It just calls the callback parameter and passes
            // an array of Task<HtmlDocument>
            Action<Task<HtmlDocument>[]> completedAction = (completedTasks) => { completionCallback(completedTasks); };

            //Trace.TraceInformation("About to begin ContinueWithAll");
            if (myTasks.Length > 0)
            {
                // Launch all of myTasks and call completedAction when done
                Task.Factory.ContinueWhenAll<HtmlDocument>(myTasks, completedAction);
            }
            else
            {
                completedAction(myTasks);
            }
            //Trace.TraceInformation("Finished ContinueWithAll");
        }

        /// <summary>
        /// Gets the stream for response.
        /// </summary>
        /// <param name="response">The response.</param>
        /// <returns></returns>
        protected static Stream GetStreamForResponse(HttpWebResponse response)
        {
            Stream stream;
            switch (response.ContentEncoding.ToUpperInvariant())
            {
                case "GZIP":
                    stream = new GZipStream(response.GetResponseStream(), CompressionMode.Decompress);
                    break;
                case "DEFLATE":
                    stream = new DeflateStream(response.GetResponseStream(), CompressionMode.Decompress);
                    break;
                default:
                    stream = response.GetResponseStream();
                    break;
            }

            return stream;
        }

        /// <summary>
        /// Retrieves the page in an HtmlDocument.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns>The page content in an HtmlDocument object</returns>
        public virtual HtmlDocument RetrievePageHtml(string parameter)
        {
            HtmlDocument doc = new HtmlDocument();
            Uri productUri = this.BuildUrl(parameter); //new Uri(String.Format("http://www.lcbo.com/lcbo-ear/lcbo/product/inventory/searchResults.do?language=EN&itemNumber={0}", productNumber));
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(productUri);
            request.Method = this.RequestMethod; //WebRequestMethods.Http.Get;
            request.Accept = "text/html";
            request.Headers.Add("Accept-Encoding", "gzip,deflate");

            if (this is LiveStoreListPageRetriever)
            {
                request.CookieContainer = new CookieContainer();
                request.CookieContainer.Add(((LiveStoreListPageRetriever)this).Cookies);
                request.Referer = "http://lcbo.com/storesearch/index.shtml";
            }

            if (this is TestStoreListPageRetriever)
            {
                request.CookieContainer = new CookieContainer();
                request.CookieContainer.Add(((TestStoreListPageRetriever)this).Cookies);
                request.Referer = "http://lcbo.com/storesearch/index.shtml";
            }

            Stream requestStream = null;
            if (this.RequestMethod == WebRequestMethods.Http.Post)
            {
                string postData = this.PostParameters;
                ASCIIEncoding requestEncoding = new ASCIIEncoding();
                byte[] postDataBytes = requestEncoding.GetBytes(postData);
                request.ContentType = "application/x-www-form-urlencoded";
                request.ContentLength = postDataBytes.Length;
                requestStream = request.GetRequestStream();
                requestStream.Write(postDataBytes, 0, postDataBytes.Length);
            }

            //make request for web page
            using (HttpWebResponse myWebResponse = (HttpWebResponse)request.GetResponse())
            {
                using (Stream myWebSource = GetStreamForResponse(myWebResponse))//new StreamReader(myWebResponse.GetResponseStream()))
                {
                    string codePage = GetCodePage(myWebResponse.ContentType);
                    Encoding encoding = Encodings[codePage];
                    doc.Load(myWebSource, encoding);
                }
            }

            if (requestStream != null)
            {
                requestStream.Close();
            }
            //}

            return doc;
        }
    }
}
